﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

namespace Selenium
{
    /// <summary>
    /// Sends commands and retrieves results via HTTP (Slightly modified from Selenium-built code for Quail happyness.
    /// See: http://code.google.com/p/selenium/source/checkout
    /// </summary>
    public class QuailHttpCommandProcessor : ICommandProcessor
    {
        private readonly string _url;
        private readonly string _browserStartCommand;
        private readonly string _browserUrl;
        private string _extensionJs;

        /// <summary>
        /// The server URL, to whom we send command requests
        /// </summary>
        public string Url
        {
            get { return _url; }
        }

        /// <summary>
        /// Gets or sets the default HTTP timeout
        /// </summary>
        public int DefaultHttpTimeout { get; set; }

        /// <summary>
        /// Gets or sets the session ID used against Selenium
        /// </summary>
        public string SessionId { get; set; }

        /// <summary>
        /// Specifies a server host/port, a command to launch the browser, and a starting URL for the browser.
        /// </summary>
        /// <param name="serverHost">the host name on which the Selenium Server resides</param>
        /// <param name="serverPort">the port on which the Selenium Server is listening</param>
        /// <param name="browserStartCommand">the command string used to launch the browser, e.g. "*firefox" or "c:\\program files\\internet explorer\\iexplore.exe"</param>
        /// <param name="browserUrl">the starting URL including just a domain name.  We'll start the browser pointing at the Selenium resources on this URL,
        /// e.g. "http://www.google.com" would send the browser to "http://www.google.com/selenium-server/RemoteRunner.html"</param>
        public QuailHttpCommandProcessor(string serverHost, int serverPort, string browserStartCommand, string browserUrl)
            : this("http://" + serverHost + ":" + serverPort + "/selenium-server/driver/", browserStartCommand, browserUrl)
        {
        }

        /// <summary>
        /// Specifies the URL to the server, a command to launch the browser, and a starting URL for the browser.
        /// </summary>
        /// <param name="serverUrl">the URL of the Selenium Server Driver, e.g. "http://localhost:4444/selenium-server/driver/" (don't forget the final slash!)</param>
        /// <param name="browserStartCommand">the command string used to launch the browser, e.g. "*firefox" or "c:\\program files\\internet explorer\\iexplore.exe"</param>
        /// <param name="browserUrl">the starting URL including just a domain name.  We'll start the browser pointing at the Selenium resources on this URL,
        /// e.g. "http://www.google.com" would send the browser to "http://www.google.com/selenium-server/RemoteRunner.html"</param>
        public QuailHttpCommandProcessor(string serverUrl, string browserStartCommand, string browserUrl)
        {
            ServicePointManager.DefaultConnectionLimit = 48;
            DefaultHttpTimeout = 60000;
            _url = serverUrl;
            _browserStartCommand = browserStartCommand;
            _browserUrl = browserUrl;
            _extensionJs = "";
        }

        /// <summary>
        /// Send the specified remote command to the browser to be performed
        /// </summary>
        /// <param name="command">the remote command verb</param>
        /// <param name="args">the arguments to the remote command (depends on the verb)</param>
        /// <returns>the command result, defined by the remote JavaScript.  "getX" style
        ///		commands may return data from the browser</returns>
        public string DoCommand(string command, string[] args)
        {
            IRemoteCommand remoteCommand = new DefaultRemoteCommand(command, args);
            using (var response = (HttpWebResponse)CreateWebRequest(remoteCommand).GetResponse())
            {
                if (response.StatusCode != HttpStatusCode.OK)
                {
                    throw new SeleniumException(response.StatusDescription);
                }
                string resultBody = ReadResponse(response);
                if (!resultBody.StartsWith("OK"))
                {
                    throw new SeleniumException(resultBody);
                }
                return resultBody;

            }
        }

        /// <summary>
        /// Retrieves the body of the HTTP response
        /// </summary>
        /// <param name="response">the response object to read</param>
        /// <returns>the body of the HTTP response</returns>
        private string ReadResponse(HttpWebResponse response)
        {
            using (var reader = new StreamReader(response.GetResponseStream()))
            {
                return reader.ReadToEnd();
            }
        }

        /// <summary>
        /// Builds an HTTP request based on the specified remote Command
        /// </summary>
        /// <param name="command">the command we'll send to the server</param>
        /// <returns>an HTTP request, which will perform this command</returns>
        private WebRequest CreateWebRequest(IRemoteCommand command)
        {
            byte[] data = BuildCommandPostData(command.CommandString);

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
            request.Timeout = DefaultHttpTimeout;

            Stream rs = request.GetRequestStream();
            rs.Write(data, 0, data.Length);
            rs.Close();

            return request;
        }

        private byte[] BuildCommandPostData(string commandString)
        {
            string data = commandString;
            if (SessionId != null)
            {
                data += "&sessionId=" + SessionId;
            }
            return (new UTF8Encoding()).GetBytes(data);
        }

        /// <summary>
        /// Sets the extension Javascript to be used in the created session
        /// </summary>
        public void SetExtensionJs(string extensionJs)
        {
            _extensionJs = extensionJs;
        }

        /// <summary>
        /// Creates a new browser session
        /// </summary>
        public void Start()
        {
            string result = GetString("getNewBrowserSession", new[] { _browserStartCommand, _browserUrl, _extensionJs });
            SessionId = result;
        }

        /// <summary>
        /// Take any extra options that may be needed when creating a browser session
        /// </summary>
        /// <param name="optionString">Browser Options</param>
        public void Start(string optionString)
        {
            string result = GetString("getNewBrowserSession", new[] { _browserStartCommand, _browserUrl, _extensionJs, optionString });
            SessionId = result;
        }

        /// <summary>
        /// Wraps the version of start() that takes a string parameter, sending it the result
        /// of calling ToString() on optionsObject, which will likey be a BrowserConfigurationOptions instan
        /// </summary>
        /// <param name="optionsObject">Contains BrowserConfigurationOptions </param>
        public void Start(Object optionsObject)
        {
            Start(optionsObject.ToString());
        }

        /// <summary>
        /// Stops the previous browser session, killing the browser
        /// </summary>
        public void Stop()
        {
            DoCommand("testComplete", null);
            SessionId = null;
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public String GetString(String commandName, String[] args)
        {
            return DoCommand(commandName, args).Substring(3); // skip "OK,"
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public String[] GetStringArray(String commandName, String[] args)
        {
            String result = GetString(commandName, args);
            return ParseCsv(result);
        }

        /// <summary>
        /// Parse Selenium comma separated values.
        /// </summary>
        /// <param name="input">the comma delimited string to parse</param>
        /// <returns>the parsed comma-separated entries</returns>
        private static String[] ParseCsv(String input)
        {
            var output = new List<string>();
            var sb = new StringBuilder();
            for (int i = 0; i < input.Length; i++)
            {
                char c = input[i];
                switch (c)
                {
                    case ',':
                        output.Add(sb.ToString());
                        sb.Clear();
                        continue;
                    case '\\':
                        i++;
                        c = input[i];
                        sb.Append(c);
                        continue;
                    default:
                        sb.Append(c);
                        break;
                }
            }

            if (sb.Length > 0)
                output.Add(sb.ToString());
            return output.ToArray();
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public Decimal GetNumber(String commandName, String[] args)
        {
            var result = GetString(commandName, args);
            var d = Decimal.Parse(result);
            return d;
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public Decimal[] GetNumberArray(String commandName, String[] args)
        {
            var result = GetStringArray(commandName, args);
            var d = new Decimal[result.Length];
            for (int i = 0; i < result.Length; i++)
            {
                d[i] = Decimal.Parse(result[i]);
            }
            return d;
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public bool GetBoolean(String commandName, String[] args)
        {
            var result = GetString(commandName, args);
            if ("true".Equals(result))
            {
                return true;
            }
            if ("false".Equals(result))
            {
                return false;
            }
            throw new Exception("result was neither 'true' nor 'false': " + result);
        }

        /// <summary>
        /// Runs the specified remote accessor (getter) command and returns the retrieved result
        /// </summary>
        /// <param name="commandName">the remote Command verb</param>
        /// <param name="args">the arguments to the remote Command (depends on the verb)</param>
        /// <returns>the result of running the accessor on the browser</returns>
        public bool[] GetBooleanArray(String commandName, String[] args)
        {
            var result = GetStringArray(commandName, args);
            var b = new bool[result.Length];
            for (var i = 0; i < result.Length; i++)
            {
                if ("true".Equals(result))
                {
                    b[i] = true;
                    continue;
                }
                if ("false".Equals(result))
                {
                    b[i] = false;
                    continue;
                }
                throw new Exception("result was neither 'true' nor 'false': " + result);
            }
            return b;
        }

    }
}

